package uk.ac.manchester.tewpl.iceiface.client.explorer.ui;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.ListIterator;

import uk.ac.manchester.tewpl.iceiface.client.Functors.VoidCallback;
import uk.ac.manchester.tewpl.iceiface.client.explorer.DATA;
import uk.ac.manchester.tewpl.iceiface.client.explorer.Explorer;
import uk.ac.manchester.tewpl.iceiface.client.explorer.Utils;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.AddAirportToJourneyEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.ChangeSelectedAirportEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.ExplorerEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.ResetEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.InitEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.events.UpdatedResultsEvent;
import uk.ac.manchester.tewpl.iceiface.client.explorer.nodes.Airport;
import uk.ac.manchester.tewpl.iceiface.client.explorer.types.Mutable;
import uk.ac.manchester.tewpl.iceiface.client.explorer.types.Route;
import uk.ac.manchester.tewpl.iceiface.client.explorer.types.Uri;
import uk.ac.manchester.tewpl.iceiface.client.model.KBIndividualModel;

import com.extjs.gxt.ui.client.Registry;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.ResizeEvent;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Dialog;
import com.extjs.gxt.ui.client.widget.Info;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.menu.Menu;
import com.extjs.gxt.ui.client.widget.menu.MenuItem;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.widgetideas.graphics.client.Color;
import com.google.gwt.widgetideas.graphics.client.ImageLoader;

public class MapPanel extends UI {
	
	// Explorer
	private Explorer explorer;
	
	// Initialisation variables
	private boolean initialised = false;
	
	// Retrieve variables
	private Collection<Airport> airports = Registry.get("airports");
	private Mutable<Airport> airportSelected = Registry.get("airportSelected");
	private LinkedList<Airport> journey = Registry.get("journey");
	private ArrayList<KBIndividualModel> timePeriods = Registry.get("timePeriods");
	private Utils utils = Registry.get("utils");
	
	// Airport variables
	private int airportRadius;
	
	// Canvas variables
	private Canvas canvas;
	private int canvasHeight = 100;
	private int canvasWidth = 100;
	private Object canvasObjectHover;
	
	// Label container
	private AbsolutePanel container;
	
	// Island image variables
	private ImageElement [] islandImages;
	
	// Icon image variables
	private ImageElement resetIcon;
	private ImageElement helpIcon;
	private Point resetIconCoords = new Point(0,0);
	private Point helpIconCoords = new Point(0,0);
	
	// Instructions window
	private Dialog instructionsDialog;
	
	// Airport colours
	private final Color colorAirport = DATA.colorAirport;
	private final Color colorAirportSelected = DATA.colorAirportSelected;
	private final Color colorAirportDest = DATA.colorAirportDest;
	private final Color colorAirportDestSelected = DATA.colorAirportDestSelected;
	private final Color colorAirportDestEnroute = DATA.colorAirportDestEnroute;
	private final Color colorAirportDisabled = DATA.colorAirportDisabled;

	// Route colours
	private final Color colorRoute = DATA.colorRoute;
	private final Color colorRouteEnroute = DATA.colorRouteEnroute;

	// Island images
	private static final String[] islandUrls = DATA.islandUrls;
	private final Point [] islandOffsets = DATA.islandOffsets;
	private final float [] islandSizeModifiers = DATA.islandSizeModifiers;
	
	// Constructor
	public MapPanel(Explorer expl) {
		explorer = expl;	
	}
	
	/*
	 * Initialise the MapPanel, i.e. load island imagess
	 */
	@Override
	public void init(final VoidCallback onFinishCallback) {
		// Configure panel
		this.setHeaderVisible(false);
		
		// Create canvas
		canvas = new Canvas(canvasWidth, canvasHeight);
		canvas.setStyleName("my-canvas"); // CSS property
		
		initInstructions();
		
		// Create container
		container = new AbsolutePanel();
		this.add(container);
		
	    // Add canvas mouse event handlers
	    canvas.addMouseDownHandler(new MouseDownHandler() {
			@Override
			public void onMouseDown(MouseDownEvent event) {
				mouseDown(event);
			}
	    });	    
	    canvas.addMouseMoveHandler(new MouseMoveHandler() {
			@Override
			public void onMouseMove(MouseMoveEvent event) {
				mouseMove(event);
			}
	    });
	    
		// Resize/Move Listeners
		// Add window resize/move listener and propagate to CurriculumMap
		this.addListener(Events.Resize, new Listener<BaseEvent>() {
			public void handleEvent(BaseEvent event) {
				draw(getMapPanel().getWidth(), getMapPanel().getHeight());
			}
		});
		addListener(Events.Move, new Listener<BaseEvent>() {
			public void handleEvent(BaseEvent event) {
				//redrawCurriculumMap();
			}
		});
		
	    // Load images
		ImageLoader.loadImages(islandUrls, new ImageLoader.CallBack() {
			public void onImagesLoaded(ImageElement[] imageElements) {
				// Initialise ImageElement array
				islandImages = new ImageElement[islandUrls.length];
				
				// Copy each loaded image over to array
				for (int i=0; i<imageElements.length; i++)
					islandImages[i] = imageElements[i];
				
				// Icons urls
				String [] iconUrls = new String [] {DATA.resetIconUrl, DATA.helpIconUrl};
				// Load icons
				ImageLoader.loadImages(iconUrls, new ImageLoader.CallBack() {
					public void onImagesLoaded(ImageElement[] iconElements) {
						resetIcon = iconElements[0];
						helpIcon = iconElements[1];
						
						// Initialising done, return
						initialised = true;
						onFinishCallback.work();
						
					}
				}); // ImageLoader
			}
		}); // ImageLoader
	}
	
	@Override
	public void handle(ExplorerEvent event) {
		if (event.getClass() == InitEvent.class
			|| event.getClass() == ResetEvent.class
			|| event.getClass() == AddAirportToJourneyEvent.class
			|| event.getClass() == ChangeSelectedAirportEvent.class
			|| event.getClass() == UpdatedResultsEvent.class) {
			draw();
		}
	}
	
	/*
	 * Returns this instance of MapPanel
	 */
	private MapPanel getMapPanel() {
		return this;
	}

	/*
	 * Method fired by resize event to trigger resize and redraw of canvas
	 */
	private void draw(int width, int height) {
		if (initialised) { // Only resize canvas if it's been initialised
			canvas.resize(width, height);
		}
		canvasWidth = width;
		canvasHeight = height;
		draw(); // This must be called here, as invoked by parent window
	}
	
	/*
	 * Draw/refresh the CurriculumMap
	 */
	private void draw() {
		if (initialised) {
			// Clear container and canvas
			container.clear();
			canvas.clear();
			
			// Add canvas object to container
			container.add(canvas);
			
			// Calculate airport placement before drawing anything
			calculateAirports();
			
			// Z axis is stacked from bottom to top
			drawIslands();
			drawRoutes();
			drawAirports();
			drawIcons();
		}
		this.layout(); // Ensure window layout is up to date
	}
	
	/*
	 * Calculate the positions of the airports on the canvas
	 * TODO Add proportional resizing code!!
	 */
	private void calculateAirports() {
		
		// Assign calculated values
		int i = 0;
		for (Airport airport : airports) {
			//airport.setXY(airportOffsets[i]);
			//airport.setXY(new Point(i * 20, i * 20));
			i++;
		}
		
		// Calculate proportional size of airports
		//airportRadius = (int) ((canvasWidth < canvasHeight ? canvasWidth : canvasHeight) * 0.03);
		airportRadius = 13;

	}
	
	/*
	 * Draw airports on the canvas
	 */
	private void drawAirports() {
		// For each airport
		for (Airport airport : airports) {			
			// Select airport color
			Color airportColor = colorAirportDisabled;
			
			if (journey.size() == 0) { // If this is the initial selection
				if (airport.equals(canvasObjectHover))
					airportColor = colorAirportSelected;
				else
					airportColor = colorAirport;
			} else {
				if (utils.checkValidFinalDestination(airport)) { // If valid destination for selection
					if (airport.equals(canvasObjectHover) || airport.equals(airportSelected.get())) // Hovered or clicked
						airportColor = colorAirportDestSelected;
					else
						airportColor = colorAirportDest;
				} else { // If not a valid destination for selections
					// If this is the first and only airport in the journey
					if (journey.size() == 1 && airport.equals(journey.getFirst())) {
						airportColor = colorAirportSelected;
					} else if (airport.equals(journey.getLast())) { // If last in journey
						airportColor = colorAirportSelected;
					} else if (journey.contains(airport)) { // If enroute
						airportColor = colorAirportDestEnroute;
					}
				}
			}
			
			// NB if this point is reached, only unused airports remain, hence colorAirportDisabled assignment above
			
			canvas.setFillStyle(airportColor);

			// Draw airport on canvas
			//int x = (int) (airport.getRelativePosition().x * canvasWidth);
			//int y = (int) (airport.getRelativePosition().y * canvasHeight);

			canvas.beginPath();
				canvas.arc(airport.getX(), airport.getY() , airportRadius, 0, Math.PI*2, true);
				//canvas.arc(x, y, airportRadius, 0, Math.PI*2, true);
			canvas.fill();
			
			HTML lbl = new HTML(airport.getMapLabel() + " (" + airport.getCount() + ")");
			lbl.setStyleName("canvas-label");
			container.add(lbl, airport.getX() + airport.getLabelOffest().x, airport.getY() + airport.getLabelOffest().y);
		}
	}
	
	/*
	 * Draw island images on the canvas
	 */
	private void drawIslands() {
		
		// For each island
		for (int i=0; i<islandImages.length; i++) {
			// Calculate proportional island size, based on pre-determined scale factors
			int imgW = (int) (islandImages[i].getWidth() * islandSizeModifiers[i]);
			int imgH = (int) (islandImages[i].getHeight() * islandSizeModifiers[i]);
			
			// Draw island on canvas
			//int x = (int) (DATA.islandOffsetsRelative[i].x * canvasWidth);
			//int y = (int) (DATA.islandOffsetsRelative[i].y * canvasHeight);
			//canvas.drawImage(islandImages[i], x - (imgW/2), y - (imgH/2), imgW, imgH);
			
			canvas.drawImage(islandImages[i], islandOffsets[i].x - (imgW/2), islandOffsets[i].y - (imgH/2), imgW, imgH);
		}
	}
	
	/*
	 * Draw icons
	 */
	private void drawIcons() {
		// Calculate positions
		resetIconCoords.y = helpIcon.getHeight() + 10;
		helpIconCoords.y = 5;
		resetIconCoords.x = canvasWidth - resetIcon.getWidth() - 5;
		helpIconCoords.x = canvasWidth - helpIcon.getWidth() - 5;
		
		// Draw icons
		canvas.drawImage(resetIcon, resetIconCoords.x, resetIconCoords.y);
		canvas.drawImage(helpIcon, helpIconCoords.x, helpIconCoords.y);
	}
	
	/*
	 * Verify routes stored in the Routes array, and draw as apt.
	 */
	private void drawRoutes() {
		ListIterator<Airport> iter = journey.listIterator();
		while (iter.hasNext()) {
			Airport airport = iter.next();
			if (iter.hasNext()) { // If the airport has a destination
				// Draw a route between airport and it's destination
				drawRoute(airport, iter.next(), colorRouteEnroute);
				iter.previous();
			} else { // If this is the final destination
				// Get all legal destinations from airport
				for (Uri uri : airport.getDestinations()) {
					Airport destination = utils.getCachedAirport(uri);
					// If destination exists on map, and hasn't
					// already been visited, draw the route
					if (utils.getCachedAirport(uri) != null	&& !journey.contains(destination)) {
						Color color;
						if (destination.equals(airportSelected.get()))
							color = colorAirportDestSelected;
						else
							color = colorRoute;
						
						drawRoute(airport, utils.getCachedAirport(uri), color);
					} // if
				} // for
			} // else
		} // while
	}
	
	private void drawRoute(Airport source, Airport destination, Color color) {
		canvas.setStrokeStyle(color);
		canvas.setLineWidth((int) (airportRadius / 3));
		
		// Get distance between points
		double dX = (destination.getX() - source.getX());
		double dY = (destination.getY() - source.getY());
		double L = Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2));
		
		// Calculate points of bezier
		Point cp1 = new Point((int)(L/3), (int) -L/5);
		Point cp2 = new Point((int)(2*L/3), (int) -L/5);
		
		// Draw curve
		canvas.saveContext();
			canvas.translate(source.getX(), source.getY()); // Translate 0,0 to source(x,y)
			if (source.getX() > destination.getX()) // Rotate curve accordingly (i.e. trig limits)
				canvas.rotate(Math.asin(-dY/L));
			else
				canvas.rotate(Math.asin(dY/L));
			
			canvas.beginPath();
				canvas.moveTo(0, 0);
				if (source.getX() > destination.getX()) // Draw curve from 0,0 to (-)L, via cp1+2
					canvas.cubicCurveTo(-cp1.x, cp1.y, -cp2.x, cp2.y, (int) -L, 0);
				else
					canvas.cubicCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, (int) L, 0);
			canvas.stroke();
		canvas.restoreContext();
	}
		

//		if (routes.size() > 0) { // If there are routes
//			canvas.setLineWidth((int) (airportRadius / 4));
//			// For each route
//			for (Route route : routes) {
//				// Retrieve the airports from the map
//				Airport source = utils.getCachedAirport(route.getSource());
//				Airport destination = utils.getCachedAirport(route.getDestination());
//				// If destination airport is drawn on the canvas
//				if (source != null && destination != null) {
//					boolean drawRoute = true;
//					// Set color of route: is this airport a final stage destination?
//					if (source == journey.get(journey.size()-1)) {
//						if (journey.contains(destination)) {
//							drawRoute = false;
//						} else {
//							if (destination.equals(airportSelected.get()))
//								canvas.setStrokeStyle(colorAirportDestSelected);
//							else
//								canvas.setStrokeStyle(colorRoute);
//						}
//					} else {
//						canvas.setStrokeStyle(colorRouteEnroute);
//					}
//					
//					if (drawRoute) {
//						// Get distance between points
//						double dX = (destination.getX() - source.getX());
//						double dY = (destination.getY() - source.getY());
//						double L = Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2));
//						
//						// Calculate points of bezier
//						Point cp1 = new Point((int)(L/3), (int) -L/5);
//						Point cp2 = new Point((int)(2*L/3), (int) -L/5);
//						
//						// Draw curve
//						canvas.saveContext();
//							canvas.translate(source.getX(), source.getY()); // Translate 0,0 to source(x,y)
//							if (source.getX() > destination.getX()) // Rotate curve accordingly (i.e. trig limits)
//								canvas.rotate(Math.asin(-dY/L));
//							else
//								canvas.rotate(Math.asin(dY/L));
//							
//							canvas.beginPath();
//								canvas.moveTo(0, 0);
//								if (source.getX() > destination.getX()) // Draw curve from 0,0 to (-)L, via cp1+2
//									canvas.cubicCurveTo(-cp1.x, cp1.y, -cp2.x, cp2.y, (int) -L, 0);
//								else
//									canvas.cubicCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, (int) L, 0);
//							canvas.stroke();
//						canvas.restoreContext();
//					}
//
//				}
//			}
//		}
//	}
	
	/*
	 * Context menu events
	 */
	
	private void initContextMenu() {
		// Initialize menu object
		final Menu menu = new Menu();
		
		// Add items
		/*final MenuItem forwardRoutes = new MenuItem();
		forwardRoutes.setText("Show forward routes");
		menu.add(forwardRoutes);*/
		
		// Reset menu item
		final MenuItem resetMenuItem = new MenuItem("Reset Map");
		resetMenuItem.addSelectionListener(new SelectionListener<MenuEvent> () {
			@Override
			public void componentSelected(MenuEvent ce) {
				//confirmResetMap();
			}
		});
		
		menu.add(resetMenuItem);
		
/*		// Add BeforeShow listener
		menu.addListener(Events.BeforeShow, new Listener<MenuEvent>() {
			@Override
			public void handleEvent(MenuEvent e) {
				Airport airport = getAirportUnderMouse(window.el().getX(), window.el().getY());
				menu.removeAll();
				menu.add(new MenuItem("x = " + e.getX() + ", y = " + window.el().getY()));
				
				if (airport != null)
					menu.add(new MenuItem(airport.getLabel()));
			}
		});*/
		
		// Set context menu
		//mapPanel.setContextMenu(menu);
	}
	
	/* Mouse event handlers */
	
	/*
	 * Mouse movement handler
	 */
	private void mouseMove(MouseMoveEvent event) {
		// Check map has been initialised
		if (initialised) {
			
			Object previousHover = canvasObjectHover;
			canvasObjectHover = getObjectAtCoords(event.getX(), event.getY());
			
			if (canvasObjectHover != previousHover) { // If hover has changed
				if (canvasObjectHover != null) {
					if (canvasObjectHover.getClass() == Airport.class) {
						explorer.setCursor("pointer");
					} else if (canvasObjectHover.getClass() == ImageElement.class) {
						explorer.setCursor("pointer");
					}
				} else {
					explorer.setCursor("default");
				}
				
				// Redraw map with change
				draw();
			}
			
		}
	}

	/*
	 * Mouse click handler
	 */
	private void mouseDown(MouseDownEvent event) {
		// Check map has been initialised
		if (initialised) {

			// Get object that was clicked on cavas
			Object clickedObject = getObjectAtCoords(event.getX(), event.getY());
			
			if (clickedObject != null) { // If something was clicked
				if (clickedObject == resetIcon) { // If reset icon
					explorer.confirmReset();
				} else if (clickedObject == helpIcon) { // If help icon
					instructionsDialog.show();
				} else if (clickedObject.getClass() == Airport.class) { // If was an Airport
					Airport newSelected = (Airport) clickedObject;
					Airport prevSelected = airportSelected.get();
					
					if (journey.size() == 0) { // If initial airport selection
						explorer.addAirportToJourney(newSelected);
					} else if (utils.checkValidFinalDestination(newSelected)) { // If valid destination
						if (newSelected == prevSelected) { // If double click - crude due to IE not firing DBL_CLICK
							explorer.addAirportToJourney(newSelected);
						} else {
							explorer.setSelectedAirport(newSelected);
							//draw();
						}
					} else if (journey.contains(newSelected)) { // If already in tree
						explorer.setSelectedAirport(newSelected);
						//draw();
					} else { // If not initial or a valid destination, dead airport
						explorer.reset();
						explorer.addAirportToJourney(newSelected);
					}
				} // if Airport
			} // if != null
			
		} // if initialised
	}
	
	/*
	 * Returns the object at the given co-ordinates
	 */
	private Object getObjectAtCoords(int x, int y) {
		// Check through airports
		for (Airport airport : airports) {
			if (airport.getX() - airportRadius <= x && x <= airport.getX() + airportRadius) {
				if (airport.getY() - airportRadius <= y && y <= airport.getY() + airportRadius) {
					return airport;
				} // if
			} // if
		} // for

		// Check through icons
		if (resetIconCoords.x <= x && x <= resetIconCoords.x + resetIcon.getWidth()) {
			if (resetIconCoords.y <= y && y <= resetIconCoords.y + resetIcon.getHeight()) {
				return resetIcon;
			}
		}
		
		if (helpIconCoords.x <= x && x <= helpIconCoords.x + helpIcon.getWidth()) {
			if (helpIconCoords.y <= y && y <= helpIconCoords.y + helpIcon.getHeight()) {
				return helpIcon;
			}
		}
		
		// If nothing found
		return null;
	}
	
	private void initInstructions() {
		// Create and Reset and Instructions buttons
		instructionsDialog = new Dialog();
		instructionsDialog.setHeading("Instructions");  
		instructionsDialog.setButtons(Dialog.CLOSE);
		instructionsDialog.setBlinkModal(true);
		instructionsDialog.setModal(true);
		instructionsDialog.addText("<P>&bull;&nbsp;Click on an &quot;Airport&quot; to see the possible destinations</P>" + 
				"<P>&bull;&nbsp;Select whether you wish to search for &quot;flights&quot; from that Airport or see the Airport Terminals</P>" +
				"<P>&bull;&nbsp;Terminals allow you search for flights for that particular terminal</P>");
		instructionsDialog.setHideOnButtonClick(true);
	}

}
